#!/usr/bin/env perl

# Copyright (c) 2020-2025 The Tcpdump Group
# All rights reserved.
# SPDX-License-Identifier: BSD-2-Clause
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

require 5.8.4; # Solaris 10
use sigtrap qw(die normal-signals);
use strict;
use warnings FATAL => qw(uninitialized);
use Getopt::Long;
use Time::HiRes;
use Config;
use FindBin;
use POSIX qw(WEXITSTATUS WIFEXITED);
require $FindBin::RealBin . '/TEST' . ($Config{useithreads} ? 'mt' : 'st') . '.pm';
require $FindBin::RealBin . '/TESTlib.pm';
# TESTlib.pm
use subs qw(
	file_get_contents
	get_diff_flags
	init_tmpdir
	mytmpfile
	read_config_h
	result_failed
	result_passed
	result_skipped
	result_timed_out
	run_skip_test
	skip_config_def1
	skip_config_undef
	skip_os
	test_and_report
);

my $testsdir = $FindBin::RealBin;
# These filenames use a prefix and are relative to the temporary directory.
my $filename_stdout = 'stdout.txt';
my $filename_stderr = 'stderr.txt';
my $filename_diags = 'diags.txt';

use constant {
	EX_OK => 0,
	EX_USAGE => 64,
};

#
# Make true and false work as Booleans.
#
use constant true => 1;
use constant false => 0;

# Set these later only if running any tests.
my $diff_flags;

sub usage_text {
	my $detailed = shift;
	my $myname = $FindBin::Script;

	my $ret = "Usage: ${myname} [--passed]
       (run all tests)
  or:  ${myname} --list
       (print all test labels)
  or:  ${myname} --one <test_label> [--passed]
       (run one test only)
  or:  ${myname} --config
       (print the parsed contents of config.h)
  or:  ${myname} --help
       (print the detailed help screen)

Options:
  --passed        print the passed tests as well (with timings)
";
	return $ret unless $detailed;
	$ret .= "
TCPDUMP_BIN and CONFIG_H allow to specify custom paths to respective files
if the current working directory is not the directory where the build output
files go to.  Otherwise by default this script finds the files for both
Autoconf and CMake, both in-tree and out-of-tree builds.

TESTRUN_JOBS allows to specify the number of tester threads (1 by default).
";
	return $ret;
}

my $config_h = defined $ENV{CONFIG_H} ? $ENV{CONFIG_H} : './config.h';
my $only_one = undef;
my $only_list = 0;
my $print_passed = 0;
if (! GetOptions (
	'one=s' => \$only_one,
	'list' => \$only_list,
	'passed' => \$print_passed,
	'config' => sub {
		my %config = read_config_h $config_h;
		printf "%-50s %s\n", $_, $config{$_} foreach sort keys %config;
		exit EX_OK;
	},
	'help' => sub {print STDOUT usage_text 1; exit EX_OK;},
)) {
	print STDERR usage_text 0;
	exit EX_USAGE;
};

#
# Were we told where to find tcpdump?
#
my $TCPDUMP;
if (defined $ENV{TCPDUMP_BIN}) {
	$TCPDUMP = $ENV{TCPDUMP_BIN};
} else {
	#
	# No.  Use the appropriate path.
	#
	if ($^O eq 'msys') {
		#
		# XXX - assume, for now, a Visual Studio debug build, so that
		# tcpdump is in the Debug subdirectory.
		#
		$TCPDUMP = "Debug\\tcpdump.exe"
	} else {
		$TCPDUMP = "./tcpdump"
	}
}

sub pipe_tcpdump {
	my $option = shift;
	open (OPT_PIPE, "$TCPDUMP $option |") or die "ERROR: piping tcpdump $option failed at open\n";
	my $ret = <OPT_PIPE>;
	close (OPT_PIPE) or die "ERROR: piping tcpdump $option failed at close\n";
	return $ret;
}

# Get the type of floating-point arithmetic we're doing.
my $fptype = pipe_tcpdump ('--fp-type') == '9877.895' ? 1 : 2;
printf "%s --fp-type => %s\n", $TCPDUMP, $fptype;

# Get the size of size_t in bits.
my $time_t_size = int (pipe_tcpdump '--time-t-size');
printf "%s --time-t-size => %s\n", $TCPDUMP, $time_t_size;

# Initialize now so that the skip functions in TESTlib.pm (and therefore the
# test declarations below) work as intended.
read_config_h ($config_h);

sub skip_fptype_not {
	my $val = shift;
	return $fptype != $val ? "fp-type!=$val" : '';
}

sub skip_time_t_not {
	my $val = shift;
	return $time_t_size != $val ? "time_t is not ${val}-bit" : '';
}

my @decode_tests = (
	# -------- formerly crypto.tests --------
	# Only attempt OpenSSL-specific tests when compiled with the library.
	# Reading the secret(s) from a file does not work with Capsicum.

	{
		skip => skip_config_undef ('HAVE_LIBCRYPTO'),
		name => 'esp1',
		input => '02-sunrise-sunset-esp.pcap',
		output => 'esp1.out',
		args => '-E "0x12345678@192.1.2.45 3des-cbc-hmac96:0x4043434545464649494a4a4c4c4f4f515152525454575758"'
	},

	{
		skip => skip_config_undef ('HAVE_LIBCRYPTO'),
		name => 'esp2',
		input => '08-sunrise-sunset-esp2.pcap',
		output => 'esp2.out',
		args => '-E "0x12345678@192.1.2.45 3des-cbc-hmac96:0x43434545464649494a4a4c4c4f4f51515252545457575840,0xabcdabcd@192.0.1.1 3des-cbc-hmac96:0x434545464649494a4a4c4c4f4f5151525254545757584043"'
	},

	{
		skip => skip_config_undef ('HAVE_LIBCRYPTO'),
		name => 'esp3',
		input => '02-sunrise-sunset-esp.pcap',
		output => 'esp1.out',
		args => '-E "3des-cbc-hmac96:0x4043434545464649494a4a4c4c4f4f515152525454575758"',
	},

	{
		skip => (skip_config_undef ('HAVE_LIBCRYPTO') || skip_config_def1 ('HAVE_CAPSICUM')),
		name => 'esp4',
		input => '08-sunrise-sunset-esp2.pcap',
		output => 'esp2.out',
		args => "-E 'file ${testsdir}/esp-secrets.txt'",
	},

	{
		skip => (skip_config_undef ('HAVE_LIBCRYPTO') || skip_config_def1 ('HAVE_CAPSICUM')),
		name => 'esp5',
		input => '08-sunrise-sunset-aes.pcap',
		output => 'esp5.out',
		args => "-E 'file ${testsdir}/esp-secrets.txt'",
	},

	{
		skip => (skip_config_undef ('HAVE_LIBCRYPTO') || skip_config_def1 ('HAVE_CAPSICUM')),
		name => 'espudp1',
		input => 'espudp1.pcap',
		output => 'espudp1.out',
		args => "-E 'file ${testsdir}/esp-secrets.txt'",
	},

	{
		skip => (skip_config_undef ('HAVE_LIBCRYPTO') || skip_config_def1 ('HAVE_CAPSICUM')),
		name => 'ikev2pI2',
		input => 'ikev2pI2.pcap',
		output => 'ikev2pI2.out',
		args => "-v -v -v -v -E 'file ${testsdir}/ikev2pI2-secrets.txt'",
	},

	{
		skip => (skip_config_undef ('HAVE_LIBCRYPTO') || skip_config_def1 ('HAVE_CAPSICUM')),
		name => 'isakmp4',
		input => 'isakmp4500.pcap',
		output => 'isakmp4.out',
		args => "-E 'file ${testsdir}/esp-secrets.txt'",
	},

	{
		skip => skip_config_undef ('HAVE_LIBCRYPTO'),
		name => 'bgp-as-path-oobr-ssl',
		input => 'bgp-as-path-oobr.pcap',
		output => 'bgp-as-path-oobr-ssl.out',
		args => '-vvv -e'
	},

	{
		skip => skip_config_undef ('HAVE_LIBCRYPTO'),
		name => 'bgp-aigp-oobr-ssl',
		input => 'bgp-aigp-oobr.pcap',
		output => 'bgp-aigp-oobr-ssl.out',
		args => '-vvv -e'
	},

	{
		skip => skip_config_def1 ('HAVE_LIBCRYPTO'),
		name => 'bgp-as-path-oobr-nossl',
		input => 'bgp-as-path-oobr.pcap',
		output => 'bgp-as-path-oobr-nossl.out',
		args => '-vvv -e'
	},

	{
		skip => skip_config_def1 ('HAVE_LIBCRYPTO'),
		name => 'bgp-aigp-oobr-nossl',
		input => 'bgp-aigp-oobr.pcap',
		output => 'bgp-aigp-oobr-nossl.out',
		args => '-vvv -e'
	},

	# -------- formerly isis-seg-fault-1-v.tests --------
	# This "verbose" ISIS protocol test involves a float calculation that
	# may produce a slightly different result depending on the compiler and
	# the version of the instruction set for which it's generating code (see
	# GitHub issue #333 for another example). The test is done only if we have
	# a floating-point type, as reported by "./tcpdump --fp-type", of FPTYPE1.
	#
	# XXX - this works on my 32-bit x86 Linux virtual machine, so do this
	# regardless of the floating-point type, so always do this.  If it
	# fails on some platform, we'll need to tweak tcpdump and tests/TESTrun
	# to check for *that* floating-point difference.

	{
		name => 'isis-seg-fault-1-v',
		input => 'isis-seg-fault-1.pcapng',
		output => 'isis-seg-fault-1-v.out',
		args => '-v'
	},

	# -------- formerly lmp-v.tests --------
	# The "verbose" Link Management Protocol test involves a float calculation that
	# may produce a slightly different result depending on the compiler and the
	# version of the instruction set for which it's generating code (see GitHub
	# issue #333). The test is done with an output file that depends on the
	# floating-point type, as reported by "./tcpdump --fp-type".

	{
		skip => skip_fptype_not (1),
		name => 'lmp-v-fptype1',
		input => 'lmp.pcap',
		output => 'lmp-v-fptype1.out',
		args => '-T lmp -v'
	},
	{
		skip => skip_fptype_not (2),
		name => 'lmp-v-fptype2',
		input => 'lmp.pcap',
		output => 'lmp-v-fptype2.out',
		args => '-T lmp -v'
	},

	# -------- formerly non-bsd.tests --------
	# This specific test fails on OpenBSD because the .pcap file uses DLT_RAW,
	# which OpenBSD treats as DLT_LOOP.

	{
		skip => skip_os ('openbsd'),
		name => 'heap-overflow-1',
		input => 'heap-overflow-1.pcap',
		output => 'heap-overflow-1.out',
		args => '-v'
	},

	# -------- formerly smb.tests --------
	# Only attempt OpenSSL-specific tests when compiled with the library.
	# Reading the secret(s) from a file does not work with Capsicum.

	# EAP tests
	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'eapon1',
		input => 'eapon1.pcap',
		output => 'eapon1.out',
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'eapon1-nosmb',
		input => 'eapon1.pcap',
		output => 'eapon1-nosmb.out',
	},

	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'eapon1-v',
		input => 'eapon1.pcap',
		output => 'eapon1-v.out',
		args => '-v'
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'eapon1-v-nosmb',
		input => 'eapon1.pcap',
		output => 'eapon1-v-nosmb.out',
		args => '-v'
	},

	# IPX/Netware packets
	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'ipx',
		input => 'ipx.pcap',
		output => 'ipx.out',
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'ipx-nosmb',
		input => 'ipx.pcap',
		output => 'ipx-nosmb.out',
	},

	# bad packets from Otto Airamo and Antti Levomäki
	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'nbns-valgrind',
		input => 'nbns-valgrind.pcap',
		output => 'nbns-valgrind.out',
		args => '-vvv -e',
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'nbns-valgrind-nosmb',
		input => 'nbns-valgrind.pcap',
		output => 'nbns-valgrind-nosmb.out',
		args => '-vvv -e',
	},

	# bad packets from Junjie Wang
	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'smb_print_trans-oobr1',
		input => 'smb_print_trans-oobr1.pcap',
		output => 'smb_print_trans-oobr1.out',
		args => '-vv',
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'smb_print_trans-oobr1-nosmb',
		input => 'smb_print_trans-oobr1.pcap',
		output => 'smb_print_trans-oobr1-nosmb.out',
		args => '-vv',
	},

	# bad packets from Philippe Antoine
	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'smb_print_trans-oobr2',
		input => 'smb_print_trans-oobr2.pcap',
		output => 'smb_print_trans-oobr2.out',
		args => '-vv',
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'smb_print_trans-oobr2-nosmb',
		input => 'smb_print_trans-oobr2.pcap',
		output => 'smb_print_trans-oobr2-nosmb.out',
		args => '-vv',
	},

	# bad packets from Luis Rocha
	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'smb_data_print-oobr',
		input => 'smb_data_print-oobr.pcapng',
		output => 'smb_data_print-oobr.out',
		args => '-vv',
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'smb_data_print-oobr-nosmb',
		input => 'smb_data_print-oobr.pcapng',
		output => 'smb_data_print-oobr-nosmb.out',
		args => '-vv',
	},

	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'smb_data_print-segv',
		input => 'smb_data_print-segv.pcap',
		output => 'smb_data_print-segv.out',
		args => '-vv',
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'smb_data_print-segv-nosmb',
		input => 'smb_data_print-segv.pcap',
		output => 'smb_data_print-segv-nosmb.out',
		args => '-vv',
	},

	# WCCP redirect over GRE
	{
		skip => skip_config_undef ('ENABLE_SMB'),
		name => 'wccp_redirect_gre',
		input => 'wccp_redirect_gre.pcap',
		output => 'wccp_redirect_gre.out',
		args => '-v',
	},

	{
		skip => skip_config_def1 ('ENABLE_SMB'),
		name => 'wccp_redirect_gre-nosmb',
		input => 'wccp_redirect_gre.pcap',
		output => 'wccp_redirect_gre-nosmb.out',
		args => '-v',
	},

	# -------- formerly time.tests --------
	# The packet time when > 2038-01-19T03:14:07Z cannot be correctly printed
	# if time_t size is 32 bits (overflow).
	# Some tests are run only if time_t is 64-bit. it depends on the
	# output of "./tcpdump --time-t-size" (32 or 64).

	# A 32-bit unsigned time_t goes until 2106-02-07T06:28:15Z.
	# All values above require a pcapng file.

	{
		name => 'time_2038',
		input => 'time_2038.pcap',
		output => 'time_2038.out',
		args => '-q'
	},
	{
		name => 'time_2038_max',
		input => 'time_2038_max.pcap',
		output => 'time_2038_max.out',
		args => '-q'
	},
	{
		skip => skip_time_t_not (64),
		name => 'time_2038_overflow',
		input => 'time_2038_overflow.pcap',
		output => 'time_2038_overflow.out',
		args => '-q'
	},
	{
		skip => skip_time_t_not (64),
		name => 'time_2039',
		input => 'time_2039.pcap',
		output => 'time_2039.out',
		args => '-q'
	},
	{
		skip => skip_time_t_not (64),
		name => 'time_2106',
		input => 'time_2106.pcap',
		output => 'time_2106.out',
		args => '-q'
	},
	{
		skip => skip_time_t_not (64),
		name => 'time_2106_max',
		input => 'time_2106_max.pcap',
		output => 'time_2106_max.out',
		args => '-q'
	},
	{
		skip => skip_time_t_not (64),
		name => 'time_2106_overflow',
		input => 'time_2106_overflow.pcapng',
		output => 'time_2106_overflow.out',
		args => '-q'
	},
	{
		skip => skip_time_t_not (64),
		name => 'time_2107',
		input => 'time_2107.pcapng',
		output => 'time_2107.out',
		args => '-q'
	},
	{
		skip => skip_time_t_not (64),
		name => 'time_2106_overflow-tt',
		input => 'time_2106_overflow.pcapng',
		output => 'time_2106_overflow-tt.out',
		args => '-tt -q SPECIAL_t'
	},
	{
		skip => skip_time_t_not (64),
		name => 'time_2107-tt',
		input => 'time_2107.pcapng',
		output => 'time_2107-tt.out',
		args => '-tt -q SPECIAL_t'
	},
	{
		skip => skip_time_t_not (64),
		name => 'lspping-fec-ldp-v',
		input => 'lspping-fec-ldp.pcap',
		output => 'lspping-fec-ldp-v.out',
		args => '-v'
	},
	{
		skip => skip_time_t_not (64),
		name => 'lspping-fec-ldp-vv',
		input => 'lspping-fec-ldp.pcap',
		output => 'lspping-fec-ldp-vv.out',
		args => '-vv'
	},
	{
		skip => skip_time_t_not (64),
		name => 'lspping-fec-rsvp-v',
		input => 'lspping-fec-rsvp.pcap',
		output => 'lspping-fec-rsvp-v.out',
		args => '-v'
	},
	{
		skip => skip_time_t_not (64),
		name => 'lspping-fec-rsvp-vv',
		input => 'lspping-fec-rsvp.pcap',
		output => 'lspping-fec-rsvp-vv.out',
		args => '-vv'
	},
	{
		skip => skip_time_t_not (64),
		name => 'ntp-v',
		input => 'ntp.pcap',
		output => 'ntp-v.out',
		args => '-v'
	},
);

sub decode_exit_status {
	my $r = shift;
	my $status;
	my $coredump = false;
	if ($^O eq 'msys') {
		#
		# On Windows, the return value of system is the lower 8
		# bits of the exit status of the process, shifted left
		# 8 bits.
		#
		# If the process crashed, rather than exiting, the
		# exit status will be one of the EXCEPTION_ values
		# listed in the documentation for the GetExceptionCode()
		# macro.
		#
		# Those are defined as STATUS_ values, which should have
		# 0xC in the topmost 4 bits (being fatal error
		# statuses); some of them have a value that fits in
		# the lower 8 bits.  We could, I guess, assume that
		# any value that 1) isn't returned by tcpdump and 2)
		# corresponds to the lower 8 bits of a STATUS_ value
		# used as an EXCEPTION_ value indicates that tcpdump
		# exited with that exception.
		#
		# However, as we're running tcpdump with system, which
		# runs the command through cmd.exe, and as cmd.exe
		# doesn't map the command's exit code to its own exit
		# code in any straightforward manner, we can't get
		# that information in any case, so there's no point
		# in trying to interpret it in that fashion.
		#
		$status = $r >> 8;
	} else {
		#
		# On UN*Xes, the return status is a POSIX as filled in
		# by wait() or waitpid().
		#
		# POSIX offers some calls for analyzing it, such as
		# WIFSIGNALED() to test whether it indicates that the
		# process was terminated by a signal, WTERMSIG() to
		# get the signal number from it, WIFEXITED() to test
		# whether it indicates that the process exited normally,
		# and WEXITSTATUS() to get the exit status from it.
		#
		# POSIX doesn't standardize core dumps, so the POSIX
		# calls can't test whether a core dump occurred.
		# However, all the UN*Xes we are likely to encounter
		# follow Research UNIX in this regard, with the exit
		# status containing either 0 or a signal number in
		# the lower 7 bits, with 0 meaning "exited rather
		# than being terminated by a signal", the "core dumped"
		# flag in the 0x80 bit, and, if the signal number is
		# 0, the exit status in the next 8 bits up.
		#
		# This should be cleaned up to use the POSIX calls
		# from the Perl library - and to define an additional
		# WCOREDUMP() call to test the "core dumped" bit and
		# use that.
		#
		# But note also that, as we're running tcpdump with
		# system, which runs the command through a shell, if
		# tcpdump crashes, we'll only know that if the shell
		# maps the signal indication and uses that as its
		# exit status.
		#
		# The good news is that the Bourne shell, and compatible
		# shells, have traditionally done that.  If the process
		# for which the shell reports the exit status terminates
		# with a signal, it adds 128 to the signal number and
		# returns that as its exit status.  (This is why the
		# "this is now working right" behavior described in a
		# comment below is occurring.)
		#
		# As tcpdump itself never returns with an exit status
		# >= 128, we can try checking for an exit status with
		# the 0x80 bit set and, if we have one, get the signal
		# number from the lower 7 bits of the exit status.  We
		# can't get the "core dumped" indication from the
		# shell's exit status; all we can do is check whether
		# there's a core file.
		#
		$coredump = $r & 127 if ($r & 128);
		# This works as intended only if the caller has removed any
		# pre-existing core dumps before running the command.
		$coredump = 'present' if (! $coredump && -f 'core');
		$status = WEXITSTATUS ($r) if WIFEXITED ($r);
	}
	return ($status, $coredump);
}

sub run_decode_test {
	my $test = shift;
	my $input = $testsdir . '/' . $test->{input};
	my $output = $testsdir . '/' . $test->{output};

	# we used to do this as a nice pipeline, but the problem is that $r fails to
	# to be set properly if the tcpdump core dumps.
	#
	# Furthermore, on Windows, fc can't read the standard input, so we
	# can't do it as a pipeline in any case.

	unlink 'core';
	my $cmdline = sprintf (
		'%s -# -n -r "%s" %s >"%s" 2>"%s"',
		$TCPDUMP,
		$input,
		$test->{options},
		mytmpfile ($filename_stdout),
		mytmpfile ($filename_stderr)
	);
	my $r;
	my $T;
	if (! $print_passed) {
		$r = system $cmdline;
	} else {
		my $t0 = Time::HiRes::time;
		$r = system $cmdline;
		$T = Time::HiRes::time - $t0;
	}

	return result_failed ('failed to run tcpdump', $!) if $r == -1;

	if ($r != 0) {
		#
		# Something other than "failed to start".
		# Something other than "tcpdump opened the file, read it, and
		# dissected all the packets".  What happened?
		#
		my ($status, $coredump) = decode_exit_status $r;
		return result_failed (
			($coredump || $status) ?
			sprintf ('exit code 0x%08x (dump: %d, code: %d)', $r, $coredump, $status) :
			sprintf ('exit code 0x%08x', $r),
			file_get_contents mytmpfile $filename_stderr
		);
	}

	#
	# $r == 0
	# Compare tcpdump's output with what we think it should be.
	#
	my $diffstat;
	$cmdline = sprintf (
		'diff %s "%s" "%s" >"%s" 2>&1',
		$diff_flags,
		$output,
		mytmpfile ($filename_stdout),
		mytmpfile ($filename_diags)
	);
	$diffstat = WEXITSTATUS (system $cmdline);
	return result_failed (
		"diff exited with $diffstat",
		file_get_contents mytmpfile $filename_diags
	) if $diffstat;

	# Anything other than the "reading from" line on stderr fails the test.
	my $failed = false;
	my $filename = mytmpfile $filename_stderr;
	open (ERRORRAW, '<', $filename) || die "ERROR: failed opening ${filename}: $!\n";
	while (<ERRORRAW>) {
		next if /^reading from file /o;
		$failed = true;
		last;
	}
	close (ERRORRAW) || die "ERROR: failed closing '$filename'";;
	return result_failed (
		'stderr present',
		file_get_contents mytmpfile $filename_stderr
	) if $failed;

	return result_passed $T;
}

sub request_test {
	my $testconfig = shift;

	return {
		label => $testconfig->{name},
		func => \&run_skip_test,
		skip => $testconfig->{skip},
	} if defined $testconfig->{skip} && $testconfig->{skip} ne '';

	my $options = defined ($testconfig->{args}) ? $testconfig->{args} : '';
	if (index ($options, 'SPECIAL_t') != -1) {
		# Hack to keep specific time options for tcp-handshake-micro-t, etc.
		# -t, -tt, etc.
		$options =~ s/ SPECIAL_t//o;
	} else {
		# No specific time option, use -tttt
		$options .= ' -tttt';
	}

	return {
		label => $testconfig->{name},
		func => \&run_decode_test,
		input => $testconfig->{input},
		options => $options,
		output => $testconfig->{output},
	};
}

my $fn_testlist = "${testsdir}/TESTLIST";
open (TESTLIST, '<', $fn_testlist) || die "ERROR: failed opening ${fn_testlist}: $!\n";
while (<TESTLIST>) {
	next if /^\#/o || /^$/o;
	my ($name, $input, $output, @options) = split;
	push @decode_tests, {
		name => $name,
		input => $input,
		output => $output,
		args => join (' ', @options)
	};
}
close (TESTLIST) || die "ERROR failed closing '$fn_testlist'";

my @ready_to_run;
for (@decode_tests) {
	next if defined ($only_one) && $only_one ne $_->{name};
	push @ready_to_run, request_test $_
}

if (! scalar @ready_to_run) {
	die "ERROR: Unknown test case '${only_one}'" if defined $only_one;
	die 'Internal error: no tests defined to run!'
}
if ($only_list) {
	print $_->{label} . "\n" foreach @ready_to_run;
	exit EX_OK;
}

$diff_flags = get_diff_flags;

#
# Force UTC, so time stamps are printed in a standard time zone, and
# tests don't have to be run in the time zone in which the output
# file was generated.
#
$ENV{TZ} = 'GMT0';

print "Running tests from ${testsdir}\n";
print "with ${TCPDUMP}, version:\n";
system ("${TCPDUMP} --version") == 0 or die "ERROR: '$TCPDUMP --version' failed to run\n";

init_tmpdir 'tcpdump_TESTrun';
exit test_and_report @ready_to_run;
